<?php
/**
 * Запуск cron команд модулей.
 * Паралельно запускает процесы в системе.
 * @author nrnwest@gmail.com
 *
 */
class Arm_Cron
{
    /**
     * Путьк php cli
     * @var $_pathPhp string
     */
    protected $_pathPhp = 'php';
    
    /**
    * 
    * @var Zend_Db_Adapter_Pdo_Mysql
    */
    private $_db;
 
    /**
    * 
    * @var Arm_Cron_Sql_Pdo_Mysql
    */
    private $_sql;
    
    
    /**
     * Регистер заблокированных файлов команд.
     * @var array
     */
    private $_fpRegister = array();
    
    /**
     * Промежуток времени через который процес будет опрошен, выполнен он или нет.
     * @var int
     */
    private $_timeoutStream = 10;
    
    /**
     * Резултат вызова процесов в системе.
     * 
     * @var string
     */
    private $_resultRun = '';
    
    /**
     * Параметер которые добавляется к cron запросам к системе.
     * @var string
     */
    const CRON_RUN_PARAM = 'arm_cron';
    
    /**
     * Конструктор
     */   
    public function __construct()
    {
        $config = Arm_Core::getConfigApplication();
        $this->_pathPhp = $config->system->path->php;
        @set_time_limit((int)$config->system->cron->maxtimerun);
        $this->_db  = Arm_Db::getAdapter();
        $this->_sql = Arm_Db::getSqlObject('Arm_Cron_Sql', $this->_db);
    }
 
    /**
     * Запуск процесов
     * 
     * @param string|array $returnArray
     */
    public function runProcess($returnArray=false)
    {
       $this->_resultWrite(Arm_Translate::_('Старт запуска cron'));
       $comands = $this->getComands();
       if($comands === array()) {    
           $this->_resultWrite(Arm_Translate::_('Нет команд для запуска'));
           if($returnArray) {
               return array();
           }
           return $this->_resultRun;
       }
       $time = time();
       $comandsRunArray = array();
       foreach ($comands as $k=>$v){
           $cmd = $v['cmd'];
           // проверка не выключена ли команда.
           if($v['on'] === '0') {
               $this->_resultWrite(Arm_Translate::_('Команда, "%s" Выключена' , $cmd));
               continue;
           }
           // проверка времени, наступило время или нет.
           $timeComand = (int)($v['period']+$v['last_run']);
           if($timeComand > $time) {
               // время не пришло выйдем
               $this->_resultWrite(Arm_Translate::_('Отклонен запуск, не пришло время, 
               команда "%s" 
               время для старта команды "%s", текущее время %s', 
                                array($cmd, date('c', $timeComand), date('c',$time))));
               continue;
           }
           
           // запираем файл команды, если файла нету создаем его.
           $fp = $this->lockFileCmd($cmd);
           if(false === $fp) {
               $this->_resultWrite(Arm_Translate::_('Команда, "%s" уже выполняется ' , $cmd));
               continue;
           }
           $this->_fpRegister[] = $fp;
           // запишем полную команду php
           $cmd = $this->_pathPhp . ' ' . Arm_Core::getPathPublicHtml() . 'index.php' . ' ' . $cmd . self::CRON_RUN_PARAM . '/1/';
           // записываем команду в массив для запуска.
           $comandsRunArray[$v['id']] = array('cmd'=>$cmd, 'id'=>$v['id'], 'cmd_oreg'=>$v['cmd']);
          // запишем команду как готовую для запуска.
       }
     
       if($comandsRunArray === array()) {
           $this->_resultWrite(Arm_Translate::_('Нет команд для запуска'));
           if($returnArray) {
               return array();
           }
           return $this->_resultRun;
       }
       
       $this->_resultWrite(Arm_Translate::_('Запуск команд, и результат их работы:'));
       $this->_resultWrite(str_repeat("*", 80));
       $result = $this->_runCmd($comandsRunArray);
     
       foreach ($result as $k=>$v) {
           // обновим время запуска команды.
           $this->updateComandLastRun($v['id']);
           if($v['returnValue'] !== 0){
               // пишем в лог ошибку
               Arm_Log::system('Cron Comand:"' . $v['cmd_oreg'] . '" Error: ' . $v['error'], null , Zend_Log::CRIT);
               $this->_resultWrite(Arm_Translate::_('Ошибка! Команда:'));
               $this->_resultWrite(Arm_Translate::_('Полученная команда с базы данных %s', "\n" . $v['cmd_oreg']));
               $this->_resultWrite(Arm_Translate::_('Команда выполненая %s', "\n" . $v['cmd']));
               $this->_resultWrite(Arm_Translate::_('Ошибка: %s', "\n" . $v['error']));
               continue;
           }           
           $this->_resultWrite(Arm_Translate::_('Команда id: %s', $v['id']));
           $this->_resultWrite(Arm_Translate::_('Полученная команда с базы данных: %s', "\n" . $v['cmd_oreg']));
           $this->_resultWrite(Arm_Translate::_('Команда выполненая: %s', "\n" . $v['cmd']));
           $this->_resultWrite(Arm_Translate::_('Время выполнения: %s секунд', $v['time_end']-$v['time_start']));
           $this->_resultWrite(Arm_Translate::_('Результат:'));
           $this->_resultWrite($v['result']);
           $this->_resultWrite('');
           $this->_resultWrite(str_repeat("*", 80));
           $this->_resultWrite("\n\n");
       }
       // закрываем заблокированные файлы.
       foreach ($this->_fpRegister as $fp) {
           flock($fp, LOCK_UN);
           fclose($fp);
       }
       if($returnArray) {
           return  $result;
       }
       return $this->_resultRun;
    }
    
    /**
     * Запуск команд.
     * Возвращает масив с результатами выполнения каждой команды, каждый масив команды
     * содержит:
     * <code>
     *  [cmd] => команда которая была выполнена
     *  [id] =>  индефикатор команды в системе db:admin_core
     *  [cmd_oreg] => команда полученная с базы данных
     *  [time_start] => время старта
     *  [result] => результат выполнения
     *  [error] => ошибка 
     *  [returnValue] => если int 1 была ошибка
     *  [time_end] => время окончания
     * </code>
     * 
     * 
     * @link http://www.ibm.com/developerworks/library/os-php-multitask/index.html?S_TACT=105AGX99&S_CMP=CP
     * @param array $cmdArray
     */
    protected function _runCmd($cmdArray)
    {       
          $streams=array();
          $handles=array();
          foreach ($cmdArray as $id=>$v) {
              $descriptorspec=array(
                  0 => array("pipe", "r"),
                  1 => array("pipe", "w"),
                  2 => array("pipe", "w")
              );
              $handles[$id]=proc_open($v['cmd'], $descriptorspec, $pipes);
              $streams[$id]=$pipes[1];
              $allPipes[$id]=$pipes;
          }
          
          while (count($streams)) { 
              $read=$streams; 
              $w=null;
              $e=null;
              stream_select($read, $w, $e, $this->_timeoutStream); 
              foreach ($read as $r) { 
                  $id=array_search($r, $streams); 
                  if(array_key_exists('time_start', $cmdArray[$id]) === false) {
                      $cmdArray[$id]['time_start'] = time();
                  }
                  $cmdArray[$id]['result'] = stream_get_contents($allPipes[$id][1]);
                  if (feof($r)) {
                      fclose($allPipes[$id][0]);
                      fclose($allPipes[$id][1]);                 
                      $cmdArray[$id]['error'] = trim(fread($allPipes[$id][2], 1024));
                      fclose($allPipes[$id][2]);
                      $cmdArray[$id]['returnValue']=proc_close($handles[$id]);
                      $cmdArray[$id]['time_end'] = time();
                      unset($streams[$id]); 
                  }
              } 
          } 
        return $cmdArray;
    }
    
    /**
     * Метод создает файл на основе команды cmd если его нет, 
     * и пробует его запереть.
     * 
     * @param string $cmd
     */
    public function lockFileCmd($cmd)
    {
       $path = Arm_Core::getPathApplication() . 'temp' . DIRECTORY_SEPARATOR . 'cron' . DIRECTORY_SEPARATOR;
       if(false === is_dir($path)) {
           mkdir($path);
       }
       
       $path .= str_replace('/', '_', $cmd);
       // создаем указатель на файл.
        @$fp = fopen($path, 'w+');
        if(false === $fp) {
            return false;
        }
        
        // блокируем файл
        if(true === flock($fp, LOCK_EX+LOCK_NB)) {
            return $fp;
        }
        fclose($fp);
        return false;
    }
    
    /**
     * Вернет комадны для cron записанные в системе
     * 
     * @return array
     */
    public function getComands($idModule=null, $returnSelect = false)
    {
      return  $this->_sql->getComands($idModule, $returnSelect);
    }
    
    /**
     * Возвращает данные про одну команду.
     * @param int $idComand
     */
    public function getComand($idComand)
    {
        return $this->_sql->getComand($idComand);
    }
    
    /**
     * Запись команды cron, в систему.
     * 
     * 
     * @param int $idModule id модуля в системе.
     * @param string $cmd команда, пример /default/cli/test/key1/param1/
     * @param int $period период в секундах через сколько запускать команду
     */
    public function addComand($idModule, $cmd, $period, $description)
    {
        $cmd = '/' . trim($cmd, '/\\') . '/';
        return $this->_sql->addComand($idModule, $cmd, $period, $description);
    }
    
    /**
     * Обновления времени последнего запуска команды.
     * 
     * @param int $idComand
     */
    public function updateComandLastRun($idComand)
    {
       $this->_sql->updateComand($idComand, array('last_run'=>time()));
       return;
    }
    
    /**
     * Обновления команды.
     * @param int $idComand
     * @param array $data
     */
    public function updateComand($idComand, $data)
    {
       $this->_sql->updateComand($idComand, $data);
       return;
    }
    
    /**
     * Удаляем команду.
     * 
     * @param int $idComand
     */
    public function deleteComand($idComand)
    {
        $this->_sql->deleteComand($idComand);
        return;
    }
    
    /**
     * Удаляет все команды модуля.
     * @param int $idModule
     */
    public function deleteComands($idModule)
    {
       $comands = $this->getComands($idModule);
       foreach ($comands as $v) {
           $this->deleteComand($v['id']);
       }
       return;
    }
    
    /**
     * Запишет все команды модуля, перед этим удалив команды.
     * 
     * @param int $idModule
     */
    public function writeComandsModule($idModule)
    {
        $this->deleteComands($idModule);
        $module = Arm_Module::getInstance()->getNameModule($idModule);
        $path = Arm_Core::getPathModule($module) . 'install/cron.php';
        if(is_file($path) === false) {
            return false;
        }
        $cron = require $path;
        foreach ($cron as $v) {
            $this->addComand($idModule, $v['cmd'], $v['period'], $v['description']);
        }
        return;
    }
    
    /**
     * Пишет в результатирующие переменую.
     * 
     * @param string $message
     */
    protected function _resultWrite($message)
    {
        $this->_resultRun .= $message . "\n";
        return;
    }
    
    /**
     * Запись команд cron в директорию install модуля.
     * 
     * @param int    $idModule
     * @param string $module
     */
    public function writeDampComandsModule($idModule, $module)
    {
        $data = $this->getComands($idModule);
        $result = array();
        foreach ($data as $v) {
            $result[] = array('cmd'=>$v['cmd'], 'period'=>$v['period'], 'description'=>$v['description']);
        }
        $path = Arm_Core::getPathModule($module) . 'install/cron.php';
        $write = new Zend_Config_Writer_Array();
        $write->write($path, new Zend_Config($result));
        return; 
    }
    
    
}